/*
 * Copyright 2002-2018 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.springframework.web.reactive.function.server;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.HttpCookie;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.codec.HttpMessageReader;
import org.springframework.http.codec.multipart.Part;
import org.springframework.http.server.PathContainer;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.MultiValueMap;
import org.springframework.web.reactive.function.BodyExtractor;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebSession;
import org.springframework.web.util.UriBuilder;
import org.springframework.web.util.UriUtils;
import org.springframework.web.util.pattern.PathPattern;
import org.springframework.web.util.pattern.PathPatternParser;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import java.net.InetSocketAddress;
import java.net.URI;
import java.security.Principal;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Function;
import java.util.function.Predicate;

/**
 * Implementations of {@link RequestPredicate} that implement various useful
 * request matching operations, such as matching based on path, HTTP method, etc.
 *
 * @author Arjen Poutsma
 * @since 5.0
 */
public abstract class RequestPredicates {

    private static final Log logger = LogFactory.getLog(RequestPredicates.class);

    private static final PathPatternParser DEFAULT_PATTERN_PARSER = new PathPatternParser();


    /**
     * Return a {@code RequestPredicate} that always matches.
     *
     * @return a predicate that always matches
     */
    public static RequestPredicate all() {
        return request -> true;
    }


    /**
     * Return a {@code RequestPredicate} that matches if the request's
     * HTTP method is equal to the given method.
     *
     * @param httpMethod the HTTP method to match against
     * @return a predicate that tests against the given HTTP method
     */
    public static RequestPredicate method(HttpMethod httpMethod) {
        return new HttpMethodPredicate(httpMethod);
    }

    /**
     * Return a {@code RequestPredicate} that matches if the request's
     * HTTP method is equal to one the of the given methods.
     *
     * @param httpMethods the HTTP methods to match against
     * @return a predicate that tests against the given HTTP methods
     * @since 5.1
     */
    public static RequestPredicate methods(HttpMethod... httpMethods) {
        return new HttpMethodPredicate(httpMethods);
    }

    /**
     * Return a {@code RequestPredicate} that tests the request path
     * against the given path pattern.
     *
     * @param pattern the pattern to match to
     * @return a predicate that tests against the given path pattern
     */
    public static RequestPredicate path(String pattern) {
        Assert.notNull(pattern, "'pattern' must not be null");
        return pathPredicates(DEFAULT_PATTERN_PARSER).apply(pattern);
    }

    /**
     * Return a function that creates new path-matching {@code RequestPredicates}
     * from pattern Strings using the given {@link PathPatternParser}.
     * <p>This method can be used to specify a non-default, customized
     * {@code PathPatternParser} when resolving path patterns.
     *
     * @param patternParser the parser used to parse patterns given to the returned function
     * @return a function that resolves a pattern String into a path-matching
     * {@code RequestPredicates} instance
     */
    public static Function<String, RequestPredicate> pathPredicates(PathPatternParser patternParser) {
        Assert.notNull(patternParser, "PathPatternParser must not be null");
        return pattern -> new PathPatternPredicate(patternParser.parse(pattern));
    }

    /**
     * Return a {@code RequestPredicate} that tests the request's headers
     * against the given headers predicate.
     *
     * @param headersPredicate a predicate that tests against the request headers
     * @return a predicate that tests against the given header predicate
     */
    public static RequestPredicate headers(Predicate<ServerRequest.Headers> headersPredicate) {
        return new HeadersPredicate(headersPredicate);
    }

    /**
     * Return a {@code RequestPredicate} that tests if the request's
     * {@linkplain ServerRequest.Headers#contentType() content type} is
     * {@linkplain MediaType#includes(MediaType) included} by any of the given media types.
     *
     * @param mediaTypes the media types to match the request's content type against
     * @return a predicate that tests the request's content type against the given media types
     */
    public static RequestPredicate contentType(MediaType... mediaTypes) {
        Assert.notEmpty(mediaTypes, "'mediaTypes' must not be empty");
        return new ContentTypePredicate(mediaTypes);
    }

    /**
     * Return a {@code RequestPredicate} that tests if the request's
     * {@linkplain ServerRequest.Headers#accept() accept} header is
     * {@linkplain MediaType#isCompatibleWith(MediaType) compatible} with any of the given media types.
     *
     * @param mediaTypes the media types to match the request's accept header against
     * @return a predicate that tests the request's accept header against the given media types
     */
    public static RequestPredicate accept(MediaType... mediaTypes) {
        Assert.notEmpty(mediaTypes, "'mediaTypes' must not be empty");
        return new AcceptPredicate(mediaTypes);
    }

    /**
     * Return a {@code RequestPredicate} that matches if request's HTTP method is {@code GET}
     * and the given {@code pattern} matches against the request path.
     *
     * @param pattern the path pattern to match against
     * @return a predicate that matches if the request method is GET and if the given pattern
     * matches against the request path
     */
    public static RequestPredicate GET(String pattern) {
        return method(HttpMethod.GET).and(path(pattern));
    }

    /**
     * Return a {@code RequestPredicate} that matches if request's HTTP method is {@code HEAD}
     * and the given {@code pattern} matches against the request path.
     *
     * @param pattern the path pattern to match against
     * @return a predicate that matches if the request method is HEAD and if the given pattern
     * matches against the request path
     */
    public static RequestPredicate HEAD(String pattern) {
        return method(HttpMethod.HEAD).and(path(pattern));
    }

    /**
     * Return a {@code RequestPredicate} that matches if request's HTTP method is {@code POST}
     * and the given {@code pattern} matches against the request path.
     *
     * @param pattern the path pattern to match against
     * @return a predicate that matches if the request method is POST and if the given pattern
     * matches against the request path
     */
    public static RequestPredicate POST(String pattern) {
        return method(HttpMethod.POST).and(path(pattern));
    }

    /**
     * Return a {@code RequestPredicate} that matches if request's HTTP method is {@code PUT}
     * and the given {@code pattern} matches against the request path.
     *
     * @param pattern the path pattern to match against
     * @return a predicate that matches if the request method is PUT and if the given pattern
     * matches against the request path
     */
    public static RequestPredicate PUT(String pattern) {
        return method(HttpMethod.PUT).and(path(pattern));
    }

    /**
     * Return a {@code RequestPredicate} that matches if request's HTTP method is {@code PATCH}
     * and the given {@code pattern} matches against the request path.
     *
     * @param pattern the path pattern to match against
     * @return a predicate that matches if the request method is PATCH and if the given pattern
     * matches against the request path
     */
    public static RequestPredicate PATCH(String pattern) {
        return method(HttpMethod.PATCH).and(path(pattern));
    }

    /**
     * Return a {@code RequestPredicate} that matches if request's HTTP method is {@code DELETE}
     * and the given {@code pattern} matches against the request path.
     *
     * @param pattern the path pattern to match against
     * @return a predicate that matches if the request method is DELETE and if the given pattern
     * matches against the request path
     */
    public static RequestPredicate DELETE(String pattern) {
        return method(HttpMethod.DELETE).and(path(pattern));
    }

    /**
     * Return a {@code RequestPredicate} that matches if request's HTTP method is {@code OPTIONS}
     * and the given {@code pattern} matches against the request path.
     *
     * @param pattern the path pattern to match against
     * @return a predicate that matches if the request method is OPTIONS and if the given pattern
     * matches against the request path
     */
    public static RequestPredicate OPTIONS(String pattern) {
        return method(HttpMethod.OPTIONS).and(path(pattern));
    }

    /**
     * Return a {@code RequestPredicate} that matches if the request's path has the given extension.
     *
     * @param extension the path extension to match against, ignoring case
     * @return a predicate that matches if the request's path has the given file extension
     */
    public static RequestPredicate pathExtension(String extension) {
        Assert.notNull(extension, "'extension' must not be null");
        return new PathExtensionPredicate(extension);
    }

    /**
     * Return a {@code RequestPredicate} that matches if the request's path matches the given
     * predicate.
     *
     * @param extensionPredicate the predicate to test against the request path extension
     * @return a predicate that matches if the given predicate matches against the request's path
     * file extension
     */
    public static RequestPredicate pathExtension(Predicate<String> extensionPredicate) {
        return new PathExtensionPredicate(extensionPredicate);
    }

    /**
     * Return a {@code RequestPredicate} that matches if the request's query parameter of the given name
     * has the given value.
     *
     * @param name  the name of the query parameter to test against
     * @param value the value of the query parameter to test against
     * @return a predicate that matches if the query parameter has the given value
     * @see ServerRequest#queryParam(String)
     * @since 5.0.7
     */
    public static RequestPredicate queryParam(String name, String value) {
        return new QueryParamPredicate(name, value);
    }

    /**
     * Return a {@code RequestPredicate} that tests the request's query parameter of the given name
     * against the given predicate.
     *
     * @param name      the name of the query parameter to test against
     * @param predicate predicate to test against the query parameter value
     * @return a predicate that matches the given predicate against the query parameter of the given name
     * @see ServerRequest#queryParam(String)
     */
    public static RequestPredicate queryParam(String name, Predicate<String> predicate) {
        return new QueryParamPredicate(name, predicate);
    }


    private static void traceMatch(String prefix, Object desired, @Nullable Object actual, boolean match) {
        if (logger.isTraceEnabled()) {
            logger.trace(String.format("%s \"%s\" %s against value \"%s\"",
                    prefix, desired, match ? "matches" : "does not match", actual));
        }
    }

    private static void restoreAttributes(ServerRequest request, Map<String, Object> attributes) {
        request.attributes().clear();
        request.attributes().putAll(attributes);
    }

    private static Map<String, String> mergePathVariables(Map<String, String> oldVariables,
                                                          Map<String, String> newVariables) {

        if (!newVariables.isEmpty()) {
            Map<String, String> mergedVariables = new LinkedHashMap<>(oldVariables);
            mergedVariables.putAll(newVariables);
            return mergedVariables;
        } else {
            return oldVariables;
        }
    }

    private static PathPattern mergePatterns(@Nullable PathPattern oldPattern, PathPattern newPattern) {
        if (oldPattern != null) {
            return oldPattern.combine(newPattern);
        } else {
            return newPattern;
        }

    }


    /**
     * Receives notifications from the logical structure of request predicates.
     */
    public interface Visitor {

        /**
         * Receive notification of an HTTP method predicate.
         *
         * @param methods the HTTP methods that make up the predicate
         * @see RequestPredicates#method(HttpMethod)
         */
        void method(Set<HttpMethod> methods);

        /**
         * Receive notification of an path predicate.
         *
         * @param pattern the path pattern that makes up the predicate
         * @see RequestPredicates#path(String)
         */
        void path(String pattern);

        /**
         * Receive notification of an path extension predicate.
         *
         * @param extension the path extension that makes up the predicate
         * @see RequestPredicates#pathExtension(String)
         */
        void pathExtension(String extension);

        /**
         * Receive notification of a HTTP header predicate.
         *
         * @param name  the name of the HTTP header to check
         * @param value the desired value of the HTTP header
         * @see RequestPredicates#headers(Predicate)
         * @see RequestPredicates#contentType(MediaType...)
         * @see RequestPredicates#accept(MediaType...)
         */
        void header(String name, String value);

        /**
         * Receive notification of a query parameter predicate.
         *
         * @param name  the name of the query parameter
         * @param value the desired value of the parameter
         * @see RequestPredicates#queryParam(String, String)
         */
        void queryParam(String name, String value);

        /**
         * Receive first notification of a logical AND predicate.
         * The first subsequent notification will contain the left-hand side of the AND-predicate;
         * followed by {@link #and()}, followed by the right-hand side, followed by {@link #endAnd()}.
         *
         * @see RequestPredicate#and(RequestPredicate)
         */
        void startAnd();

        /**
         * Receive "middle" notification of a logical AND predicate.
         * The following notification contains the right-hand side, followed by {@link #endAnd()}.
         *
         * @see RequestPredicate#and(RequestPredicate)
         */
        void and();

        /**
         * Receive last notification of a logical AND predicate.
         *
         * @see RequestPredicate#and(RequestPredicate)
         */
        void endAnd();

        /**
         * Receive first notification of a logical OR predicate.
         * The first subsequent notification will contain the left-hand side of the OR-predicate;
         * the second notification contains the right-hand side, followed by {@link #endOr()}.
         *
         * @see RequestPredicate#or(RequestPredicate)
         */
        void startOr();

        /**
         * Receive "middle" notification of a logical OR predicate.
         * The following notification contains the right-hand side, followed by {@link #endOr()}.
         *
         * @see RequestPredicate#or(RequestPredicate)
         */
        void or();

        /**
         * Receive last notification of a logical OR predicate.
         *
         * @see RequestPredicate#or(RequestPredicate)
         */
        void endOr();

        /**
         * Receive first notification of a negated predicate.
         * The first subsequent notification will contain the negated predicated, followed
         * by {@link #endNegate()}.
         *
         * @see RequestPredicate#negate()
         */
        void startNegate();

        /**
         * Receive last notification of a negated predicate.
         *
         * @see RequestPredicate#negate()
         */
        void endNegate();

        /**
         * Receive first notification of an unknown predicate.
         */
        void unknown(RequestPredicate predicate);
    }


    private static class HttpMethodPredicate implements RequestPredicate {

        private final Set<HttpMethod> httpMethods;

        public HttpMethodPredicate(HttpMethod httpMethod) {
            Assert.notNull(httpMethod, "HttpMethod must not be null");
            this.httpMethods = EnumSet.of(httpMethod);
        }

        public HttpMethodPredicate(HttpMethod... httpMethods) {
            Assert.notEmpty(httpMethods, "HttpMethods must not be empty");

            this.httpMethods = EnumSet.copyOf(Arrays.asList(httpMethods));
        }

        @Override
        public boolean test(ServerRequest request) {
            boolean match = this.httpMethods.contains(request.method());
            traceMatch("Method", this.httpMethods, request.method(), match);
            return match;
        }

        @Override
        public void accept(Visitor visitor) {
            visitor.method(Collections.unmodifiableSet(this.httpMethods));
        }

        @Override
        public String toString() {
            if (this.httpMethods.size() == 1) {
                return this.httpMethods.iterator().next().toString();
            } else {
                return this.httpMethods.toString();
            }
        }
    }


    private static class PathPatternPredicate implements RequestPredicate {

        private final PathPattern pattern;

        public PathPatternPredicate(PathPattern pattern) {
            Assert.notNull(pattern, "'pattern' must not be null");
            this.pattern = pattern;
        }

        private static void mergeAttributes(ServerRequest request, Map<String, String> variables,
                                            PathPattern pattern) {
            Map<String, String> pathVariables = mergePathVariables(request.pathVariables(), variables);
            request.attributes().put(RouterFunctions.URI_TEMPLATE_VARIABLES_ATTRIBUTE,
                    Collections.unmodifiableMap(pathVariables));

            pattern = mergePatterns(
                    (PathPattern) request.attributes().get(RouterFunctions.MATCHING_PATTERN_ATTRIBUTE),
                    pattern);
            request.attributes().put(RouterFunctions.MATCHING_PATTERN_ATTRIBUTE, pattern);
        }

        @Override
        public boolean test(ServerRequest request) {
            PathContainer pathContainer = request.pathContainer();
            PathPattern.PathMatchInfo info = this.pattern.matchAndExtract(pathContainer);
            traceMatch("Pattern", this.pattern.getPatternString(), request.path(), info != null);
            if (info != null) {
                mergeAttributes(request, info.getUriVariables(), this.pattern);
                return true;
            } else {
                return false;
            }
        }

        @Override
        public Optional<ServerRequest> nest(ServerRequest request) {
            return Optional.ofNullable(this.pattern.matchStartOfPath(request.pathContainer()))
                    .map(info -> new SubPathServerRequestWrapper(request, info, this.pattern));
        }

        @Override
        public void accept(Visitor visitor) {
            visitor.path(this.pattern.getPatternString());
        }

        @Override
        public String toString() {
            return this.pattern.getPatternString();
        }
    }


    private static class HeadersPredicate implements RequestPredicate {

        private final Predicate<ServerRequest.Headers> headersPredicate;

        public HeadersPredicate(Predicate<ServerRequest.Headers> headersPredicate) {
            Assert.notNull(headersPredicate, "Predicate must not be null");
            this.headersPredicate = headersPredicate;
        }

        @Override
        public boolean test(ServerRequest request) {
            return this.headersPredicate.test(request.headers());
        }

        @Override
        public String toString() {
            return this.headersPredicate.toString();
        }
    }

    private static class ContentTypePredicate extends HeadersPredicate {

        private final Set<MediaType> mediaTypes;

        public ContentTypePredicate(MediaType... mediaTypes) {
            this(new HashSet<>(Arrays.asList(mediaTypes)));
        }

        private ContentTypePredicate(Set<MediaType> mediaTypes) {
            super(headers -> {
                MediaType contentType =
                        headers.contentType().orElse(MediaType.APPLICATION_OCTET_STREAM);
                boolean match = mediaTypes.stream()
                        .anyMatch(mediaType -> mediaType.includes(contentType));
                traceMatch("Content-Type", mediaTypes, contentType, match);
                return match;
            });
            this.mediaTypes = mediaTypes;
        }

        @Override
        public void accept(Visitor visitor) {
            visitor.header(HttpHeaders.CONTENT_TYPE,
                    (this.mediaTypes.size() == 1) ?
                            this.mediaTypes.iterator().next().toString() :
                            this.mediaTypes.toString());
        }

        @Override
        public String toString() {
            return String.format("Content-Type: %s",
                    (this.mediaTypes.size() == 1) ?
                            this.mediaTypes.iterator().next().toString() :
                            this.mediaTypes.toString());
        }
    }

    private static class AcceptPredicate extends HeadersPredicate {

        private final Set<MediaType> mediaTypes;

        public AcceptPredicate(MediaType... mediaTypes) {
            this(new HashSet<>(Arrays.asList(mediaTypes)));
        }

        private AcceptPredicate(Set<MediaType> mediaTypes) {
            super(headers -> {
                List<MediaType> acceptedMediaTypes = acceptedMediaTypes(headers);
                boolean match = acceptedMediaTypes.stream()
                        .anyMatch(acceptedMediaType -> mediaTypes.stream()
                                .anyMatch(acceptedMediaType::isCompatibleWith));
                traceMatch("Accept", mediaTypes, acceptedMediaTypes, match);
                return match;
            });
            this.mediaTypes = mediaTypes;
        }

        @NonNull
        private static List<MediaType> acceptedMediaTypes(ServerRequest.Headers headers) {
            List<MediaType> acceptedMediaTypes = headers.accept();
            if (acceptedMediaTypes.isEmpty()) {
                acceptedMediaTypes = Collections.singletonList(MediaType.ALL);
            } else {
                MediaType.sortBySpecificityAndQuality(acceptedMediaTypes);
            }
            return acceptedMediaTypes;
        }

        @Override
        public void accept(Visitor visitor) {
            visitor.header(HttpHeaders.ACCEPT,
                    (this.mediaTypes.size() == 1) ?
                            this.mediaTypes.iterator().next().toString() :
                            this.mediaTypes.toString());
        }

        @Override
        public String toString() {
            return String.format("Accept: %s",
                    (this.mediaTypes.size() == 1) ?
                            this.mediaTypes.iterator().next().toString() :
                            this.mediaTypes.toString());
        }
    }


    private static class PathExtensionPredicate implements RequestPredicate {

        private final Predicate<String> extensionPredicate;

        @Nullable
        private final String extension;

        public PathExtensionPredicate(Predicate<String> extensionPredicate) {
            Assert.notNull(extensionPredicate, "Predicate must not be null");
            this.extensionPredicate = extensionPredicate;
            this.extension = null;
        }

        public PathExtensionPredicate(String extension) {
            Assert.notNull(extension, "Extension must not be null");

            this.extensionPredicate = s -> {
                boolean match = extension.equalsIgnoreCase(s);
                traceMatch("Extension", extension, s, match);
                return match;
            };
            this.extension = extension;
        }

        @Override
        public boolean test(ServerRequest request) {
            String pathExtension = UriUtils.extractFileExtension(request.path());
            return this.extensionPredicate.test(pathExtension);
        }

        @Override
        public void accept(Visitor visitor) {
            visitor.pathExtension(
                    (this.extension != null) ?
                            this.extension :
                            this.extensionPredicate.toString());
        }

        @Override
        public String toString() {
            return String.format("*.%s",
                    (this.extension != null) ?
                            this.extension :
                            this.extensionPredicate);
        }

    }


    private static class QueryParamPredicate implements RequestPredicate {

        private final String name;

        private final Predicate<String> valuePredicate;

        @Nullable
        private final String value;

        public QueryParamPredicate(String name, Predicate<String> valuePredicate) {
            Assert.notNull(name, "Name must not be null");
            Assert.notNull(valuePredicate, "Predicate must not be null");
            this.name = name;
            this.valuePredicate = valuePredicate;
            this.value = null;
        }

        public QueryParamPredicate(String name, String value) {
            Assert.notNull(name, "Name must not be null");
            Assert.notNull(value, "Value must not be null");
            this.name = name;
            this.valuePredicate = value::equals;
            this.value = value;
        }

        @Override
        public boolean test(ServerRequest request) {
            Optional<String> s = request.queryParam(this.name);
            return s.filter(this.valuePredicate).isPresent();
        }

        @Override
        public void accept(Visitor visitor) {
            visitor.queryParam(this.name,
                    (this.value != null) ?
                            this.value :
                            this.valuePredicate.toString());
        }

        @Override
        public String toString() {
            return String.format("?%s %s", this.name,
                    (this.value != null) ?
                            this.value :
                            this.valuePredicate);
        }
    }


    /**
     * {@link RequestPredicate} for where both {@code left} and {@code right} predicates
     * must match.
     */
    static class AndRequestPredicate implements RequestPredicate {

        private final RequestPredicate left;

        private final RequestPredicate right;

        public AndRequestPredicate(RequestPredicate left, RequestPredicate right) {
            Assert.notNull(left, "Left RequestPredicate must not be null");
            Assert.notNull(right, "Right RequestPredicate must not be null");
            this.left = left;
            this.right = right;
        }

        @Override
        public boolean test(ServerRequest request) {
            Map<String, Object> oldAttributes = new HashMap<>(request.attributes());

            if (this.left.test(request) && this.right.test(request)) {
                return true;
            }
            restoreAttributes(request, oldAttributes);
            return false;
        }

        @Override
        public Optional<ServerRequest> nest(ServerRequest request) {
            return this.left.nest(request).flatMap(this.right::nest);
        }

        @Override
        public void accept(Visitor visitor) {
            visitor.startAnd();
            this.left.accept(visitor);
            visitor.and();
            this.right.accept(visitor);
            visitor.endAnd();
        }

        @Override
        public String toString() {
            return String.format("(%s && %s)", this.left, this.right);
        }
    }

    /**
     * {@link RequestPredicate} that negates a delegate predicate.
     */
    static class NegateRequestPredicate implements RequestPredicate {
        private final RequestPredicate delegate;

        public NegateRequestPredicate(RequestPredicate delegate) {
            Assert.notNull(delegate, "Delegate must not be null");
            this.delegate = delegate;
        }

        @Override
        public boolean test(ServerRequest request) {
            Map<String, Object> oldAttributes = new HashMap<>(request.attributes());
            boolean result = !this.delegate.test(request);
            if (!result) {
                restoreAttributes(request, oldAttributes);
            }
            return result;
        }

        @Override
        public void accept(Visitor visitor) {
            visitor.startNegate();
            this.delegate.accept(visitor);
            visitor.endNegate();
        }

        @Override
        public String toString() {
            return "!" + this.delegate.toString();
        }
    }

    /**
     * {@link RequestPredicate} where either {@code left} or {@code right} predicates
     * may match.
     */
    static class OrRequestPredicate implements RequestPredicate {

        private final RequestPredicate left;

        private final RequestPredicate right;

        public OrRequestPredicate(RequestPredicate left, RequestPredicate right) {
            Assert.notNull(left, "Left RequestPredicate must not be null");
            Assert.notNull(right, "Right RequestPredicate must not be null");
            this.left = left;
            this.right = right;
        }

        @Override
        public boolean test(ServerRequest request) {
            Map<String, Object> oldAttributes = new HashMap<>(request.attributes());

            if (this.left.test(request)) {
                return true;
            } else {
                restoreAttributes(request, oldAttributes);
                if (this.right.test(request)) {
                    return true;
                }
            }
            restoreAttributes(request, oldAttributes);
            return false;
        }

        @Override
        public Optional<ServerRequest> nest(ServerRequest request) {
            Optional<ServerRequest> leftResult = this.left.nest(request);
            if (leftResult.isPresent()) {
                return leftResult;
            } else {
                return this.right.nest(request);
            }
        }

        @Override
        public void accept(Visitor visitor) {
            visitor.startOr();
            this.left.accept(visitor);
            visitor.or();
            this.right.accept(visitor);
            visitor.endOr();
        }


        @Override
        public String toString() {
            return String.format("(%s || %s)", this.left, this.right);
        }
    }


    private static class SubPathServerRequestWrapper implements ServerRequest {

        private final ServerRequest request;

        private final PathContainer pathContainer;

        private final Map<String, Object> attributes;

        public SubPathServerRequestWrapper(ServerRequest request,
                                           PathPattern.PathRemainingMatchInfo info, PathPattern pattern) {
            this.request = request;
            this.pathContainer = new SubPathContainer(info.getPathRemaining());
            this.attributes = mergeAttributes(request, info.getUriVariables(), pattern);
        }

        private static Map<String, Object> mergeAttributes(ServerRequest request,
                                                           Map<String, String> pathVariables, PathPattern pattern) {
            Map<String, Object> result = new ConcurrentHashMap<>(request.attributes());

            result.put(RouterFunctions.URI_TEMPLATE_VARIABLES_ATTRIBUTE,
                    mergePathVariables(request.pathVariables(), pathVariables));

            pattern = mergePatterns(
                    (PathPattern) request.attributes().get(RouterFunctions.MATCHING_PATTERN_ATTRIBUTE),
                    pattern);
            result.put(RouterFunctions.MATCHING_PATTERN_ATTRIBUTE, pattern);
            return result;
        }

        @Override
        public HttpMethod method() {
            return this.request.method();
        }

        @Override
        public String methodName() {
            return this.request.methodName();
        }

        @Override
        public URI uri() {
            return this.request.uri();
        }

        @Override
        public UriBuilder uriBuilder() {
            return this.request.uriBuilder();
        }

        @Override
        public String path() {
            return this.pathContainer.value();
        }

        @Override
        public PathContainer pathContainer() {
            return this.pathContainer;
        }

        @Override
        public Headers headers() {
            return this.request.headers();
        }

        @Override
        public MultiValueMap<String, HttpCookie> cookies() {
            return this.request.cookies();
        }

        @Override
        public Optional<InetSocketAddress> remoteAddress() {
            return this.request.remoteAddress();
        }

        @Override
        public List<HttpMessageReader<?>> messageReaders() {
            return this.request.messageReaders();
        }

        @Override
        public <T> T body(BodyExtractor<T, ? super ServerHttpRequest> extractor) {
            return this.request.body(extractor);
        }

        @Override
        public <T> T body(BodyExtractor<T, ? super ServerHttpRequest> extractor, Map<String, Object> hints) {
            return this.request.body(extractor, hints);
        }

        @Override
        public <T> Mono<T> bodyToMono(Class<? extends T> elementClass) {
            return this.request.bodyToMono(elementClass);
        }

        @Override
        public <T> Mono<T> bodyToMono(ParameterizedTypeReference<T> typeReference) {
            return this.request.bodyToMono(typeReference);
        }

        @Override
        public <T> Flux<T> bodyToFlux(Class<? extends T> elementClass) {
            return this.request.bodyToFlux(elementClass);
        }

        @Override
        public <T> Flux<T> bodyToFlux(ParameterizedTypeReference<T> typeReference) {
            return this.request.bodyToFlux(typeReference);
        }

        @Override
        public Map<String, Object> attributes() {
            return this.attributes;
        }

        @Override
        public Optional<String> queryParam(String name) {
            return this.request.queryParam(name);
        }

        @Override
        public MultiValueMap<String, String> queryParams() {
            return this.request.queryParams();
        }

        @Override
        @SuppressWarnings("unchecked")
        public Map<String, String> pathVariables() {
            return (Map<String, String>) this.attributes.getOrDefault(
                    RouterFunctions.URI_TEMPLATE_VARIABLES_ATTRIBUTE, Collections.emptyMap());

        }

        @Override
        public Mono<WebSession> session() {
            return this.request.session();
        }

        @Override
        public Mono<? extends Principal> principal() {
            return this.request.principal();
        }

        @Override
        public Mono<MultiValueMap<String, String>> formData() {
            return this.request.formData();
        }

        @Override
        public Mono<MultiValueMap<String, Part>> multipartData() {
            return this.request.multipartData();
        }

        @Override
        public ServerWebExchange exchange() {
            return this.request.exchange();
        }

        @Override
        public String toString() {
            return method() + " " + path();
        }

        private static class SubPathContainer implements PathContainer {

            private static final PathContainer.Separator SEPARATOR = () -> "/";


            private final String value;

            private final List<Element> elements;

            public SubPathContainer(PathContainer original) {
                this.value = prefixWithSlash(original.value());
                this.elements = prependWithSeparator(original.elements());
            }

            private static String prefixWithSlash(String path) {
                if (!path.startsWith("/")) {
                    path = "/" + path;
                }
                return path;
            }

            private static List<Element> prependWithSeparator(List<Element> elements) {
                List<Element> result = new ArrayList<>(elements);
                if (result.isEmpty() || !(result.get(0) instanceof Separator)) {
                    result.add(0, SEPARATOR);
                }
                return Collections.unmodifiableList(result);
            }


            @Override
            public String value() {
                return this.value;
            }

            @Override
            public List<Element> elements() {
                return this.elements;
            }
        }
    }

}
